package biz.datalk.industrialland.admin.component.shiro;

import biz.datalk.industrialland.admin.modules.common.servie.IAuthService;
import biz.datalk.industrialland.common.config.JwtCryptoCfg;
import biz.datalk.industrialland.common.def.CommonConstant;
import biz.datalk.industrialland.common.def.enums.DBYNEnum;
import biz.datalk.industrialland.common.exception.DatalkTokenException;
import biz.datalk.industrialland.common.result.UnifyResultCode;
import biz.datalk.industrialland.common.shiro.JwtToken;
import biz.datalk.industrialland.common.util.I18NUtil;
import biz.datalk.industrialland.common.util.JwtUtil;
import biz.datalk.industrialland.common.util.RedisUtil;
import biz.datalk.industrialland.mbg.mapper.SysUserRoleMapper;
import biz.datalk.industrialland.mbg.pojo.po.SysUser;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * 用户登录鉴权和获取用户授权
 *
 * @author tarofang@163.com
 * @date 2019年08月12日
 */
@Slf4j
@Component("shiroRealm")
public class ShiroRealm extends AuthorizingRealm {

    private final RedisUtil redisUtil;

    private final IAuthService authService;
    private final SysUserRoleMapper sysUserRoleMapper;

    @Autowired
    @Lazy
    public ShiroRealm(RedisUtil redisUtil, IAuthService authService, SysUserRoleMapper sysUserRoleMapper) {
        this.redisUtil = redisUtil;
        this.authService = authService;
        this.sysUserRoleMapper = sysUserRoleMapper;
    }

    /** 只能在单一地点登录 */
    @Value(value = "${datalk.login.single.place:false}")
    private boolean loginSinglePlace;


    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 功能： 获取用户权限信息，包括角色以及权限。只有当触发检测用户权限时才会调用此方法，例如checkRole,checkPermission
     * <p>
     * 鉴权时调用
     *
     * @return AuthorizationInfo 权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // log.info("————权限认证 [ roles、permissions]————");
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        return info;
    }

    /**
     * 功能： 用来进行身份认证，也就是说验证用户输入的账号和密码是否正确，获取身份验证信息，错误抛出异常
     * <p>
     * 访问时调用
     *
     * @param auth 用户身份信息 token
     *
     * @return 返回封装了用户信息的 AuthenticationInfo 实例
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        if (token == null) {
            throw new DatalkTokenException(UnifyResultCode.TOKEN_NO_EXISTS, I18NUtil.getMsg("login.token.no.exists"));
        }

        // 校验token有效性
        CustomLoginUser loginUser = this.checkUserTokenIsEffect(token);
        return new SimpleAuthenticationInfo(loginUser, token, getName());
    }

    /**
     * 校验token的有效性
     */
    protected CustomLoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
        String username = JwtUtil.getPlayload(token, JwtUtil.PAYLOAD_USERNAME);
        if (StringUtils.isBlank(username)) {
            log.debug("username is blank.");
            throw new DatalkTokenException(UnifyResultCode.TOKEN_INVALID, I18NUtil.getMsg("login.token.invalid"));
        }

        // 校验 APPKEY
        String appkey = JwtUtil.getAppkey(token);

        // 获取用户信息
        SysUser sysUser = authService.selectByUsername(username);
        if (sysUser == null) {
            log.warn("Invalid username, SysUser is null. [username={}, token={}]", username, token);
            throw new DatalkTokenException(UnifyResultCode.TOKEN_USER_NO_EXISTS, I18NUtil.getMsg("login.token.user.no.exists"));
        }

        String isDelete = sysUser.getIsDelete();
        if (!StringUtils.equals(isDelete, DBYNEnum.N.name())) {
            log.warn("SysUser is deleted. [username={}, isDelete={}, token={}]", username, isDelete, token);
            throw new DatalkTokenException(UnifyResultCode.TOKEN_USER_NO_EXISTS, I18NUtil.getMsg("login.token.user.no.exists"));
        }

        Integer status = sysUser.getStatus(); // 状态(1：正常  2：冻结 ）
        if (status == null || status != 1) {
            log.warn("SysUser is invalid. [username={}, status={}, token={}]", username, status, token);
            throw new DatalkTokenException(UnifyResultCode.TOKEN_USER_LOCK, I18NUtil.getMsg("login.token.user.lock"));
        }

        // 校验token是否超时失效 & 或者账号密码是否错误
        if (!jwtTokenRefresh(token, username, appkey)) {
            throw new DatalkTokenException(UnifyResultCode.TOKEN_TIMEOUT, I18NUtil.getMsg("login.token.timeout"));
        }

        // 获取用户的角色编码
        List<String> roleCodes = sysUserRoleMapper.selectRoleCodeBySysUserId(sysUser.getId());
        roleCodes = Optional.ofNullable(roleCodes).orElse(Lists.newArrayList());

        // 查询用户信息
        CustomLoginUser loginUser = new CustomLoginUser();
        loginUser.setUserNo(sysUser.getUsername()); // 使用 username 填充
        loginUser.setSysUser(sysUser);
        loginUser.setId(sysUser.getId());
        loginUser.setUsername(sysUser.getUsername());
        loginUser.setRealname(sysUser.getRealname());
        loginUser.setOrgCode(sysUser.getSysOrgCode());
        loginUser.setAvatar(sysUser.getAvatar());
        loginUser.setBirthday(sysUser.getBirthday());
        loginUser.setSex(sysUser.getSex());
        loginUser.setEmail(sysUser.getEmail());
        loginUser.setPhone(sysUser.getPhone());
        loginUser.setStatus(sysUser.getStatus());
        loginUser.setIsDelete(StringUtils.equals(sysUser.getIsDelete(), DBYNEnum.N.name()) ? 0 : 1);
        loginUser.setOrgCodes(Lists.newArrayList());
        loginUser.setRoleCodes(roleCodes);

        return loginUser;
    }

    /**
     * JWTToken刷新生命周期 （解决用户一直在线操作，提供Token失效问题）
     * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样)
     * 2、当该用户再次请求时，通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
     * 3、当该用户这次请求JWTToken值还在生命周期内，则会通过重新PUT的方式k、v都为Token值，缓存中的token值生命周期时间重新计算(这时候k、v值一样)
     * 4、当该用户这次请求jwt生成的token值已经超时，但该token对应cache中的k还是存在，则表示该用户一直在操作只是JWT的token失效了，程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值，该缓存生命周期重新计算
     * 5、当该用户这次请求jwt在生成的token值已经超时，并在cache中不存在对应的k，则表示该用户账户空闲超时，返回用户信息已失效，请重新登录。
     * 6、每次当返回为true情况下，都会给Response的Header中设置Authorization，该Authorization映射的v为cache对应的v值。
     * 7、注：当前端接收到Response的Header中的Authorization值会存储起来，作为以后请求token使用
     * 参考方案：https://blog.csdn.net/qq394829044/article/details/82763936
     *
     * @param username 用户唯一标识
     *
     * @return true 刷新成功 否者刷新失败
     */
    protected boolean jwtTokenRefresh(String token, String username, String appkey) {
        if (StringUtils.isBlank(token)) {
            return false;
        }

        // String tokenRedisKey = DatalkRedisKeyPrefixSerializer.REDIS_KEY_REMOVE_PREFIX_PATTERN + CommonConstant.PREFIX_USER_TOKEN.concat(token.trim());
        String tokenRedisKey = CommonConstant.PREFIX_USER_TOKEN.concat(token.trim());
        String cacheToken = redisUtil.getStr(tokenRedisKey);
        if (StringUtils.isEmpty(cacheToken)) {
            return false;
        }

        long expire = JwtCryptoCfg.getInstance().getExpire();

        // 校验token有效性
        if (!JwtUtil.verify(cacheToken)) {
            Map<String, String> payloads = JwtUtil.getPayloads(cacheToken, JwtUtil.PAYLOAD_KEYS_ALL);
            redisUtil.set(tokenRedisKey, JwtUtil.sign(payloads), expire);
        } else {
            redisUtil.set(tokenRedisKey, cacheToken, expire);
        }

        if (loginSinglePlace) {
            String latestTokenRedisKey = CommonConstant.PREFIX_USER_TOKEN_LATEST + username;
            String latestToken = redisUtil.getStr(latestTokenRedisKey);
            // 当前token与该用户最新token不一致， 挤下线
            if (!StringUtils.equals(token, latestToken)) {
                redisUtil.del(tokenRedisKey);
                throw new DatalkTokenException("你的账号已在另一个地点登录");
            }
            redisUtil.set(latestTokenRedisKey, token, expire);
        }

        return true;
    }


}
